/**
 *
 */
package com.angel.dao.generic.impl;

import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.collections15.Closure;
import org.apache.commons.collections15.CollectionUtils;
import org.apache.commons.collections15.Predicate;
import org.apache.commons.collections15.Transformer;

import com.angel.dao.generic.base.InstancesActionManager;
import com.angel.dao.generic.base.InstancesFilterManager;
import com.angel.dao.generic.exceptions.NotFoundUniqueObjectException;
import com.angel.dao.generic.exceptions.NotGenericClassesDefinedException;
import com.angel.dao.generic.exceptions.UniqueExpectedResultException;
import com.angel.dao.generic.interfaces.FindAllInstancesAction;
import com.angel.dao.generic.interfaces.FindAllInstancesFilter;
import com.angel.dao.generic.interfaces.GenericDAO;
import com.angel.dao.generic.interfaces.TransactionStrategy;

/**
 *
 * @author William
 *
 */
public abstract class AbstractGenericDAO<T extends Object, Code extends Serializable> implements GenericDAO<T, Code> {

	private TransactionStrategy transaction;

    private Class<T> persistentClass;

    private Class<Code> codeClass;

    private InstancesActionManager<T> entitiesActionManager;

    private InstancesFilterManager<T> entitiesFilterManager;

	public AbstractGenericDAO(TransactionStrategy transaction) {
		super();
		this.transaction = transaction;
		this.entitiesActionManager = new InstancesActionManager<T>();
		this.entitiesFilterManager = new InstancesFilterManager<T>();
		this.persistentClass = this.buildGenericPersistentClass();
		this.codeClass = this.buildGenericCodeClass();
	}

	public AbstractGenericDAO(TransactionStrategy transaction, Class<T> persistentClass, Class<Code> codeClass) {
		super();
		this.transaction = transaction;
		this.persistentClass = persistentClass;
		this.codeClass = codeClass;
		this.entitiesActionManager = new InstancesActionManager<T>();
		this.entitiesFilterManager = new InstancesFilterManager<T>();
	}

	@SuppressWarnings("unchecked")
	protected Class<Code> buildGenericCodeClass() {
		Type[] types = ((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments();

		//List<Class<?>> types = GenericHelper.getTypeArguments(GenericDAO.class, this.getClass());
		if(types == null || ( types != null && types.length == 0) || (types != null && types.length == 1)){
				/*|| (types != null && types.size() == 2 && types.get(1) != null))*/
			throw new NotGenericClassesDefinedException("Cannot define Generic Code Class with Generic reflection.");
		}
		return (Class<Code>) types[1];
	}

	@SuppressWarnings("unchecked")
	protected Class<T> buildGenericPersistentClass() {
		Type[] types = ((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments();
		//List<Class<?>> types = GenericHelper.getTypeArguments(AbstractGenericDAO.class, this.getClass());
		if(types == null || ( types != null && types.length == 0)){
			throw new NotGenericClassesDefinedException("Cannot define Generic Persistent Class with Generic reflection.");
		}
		return (Class<T>) types[0];
	}

	/**
	 * @return the persistentClass
	 */
	protected Class<T> getPersistentClass() {
		return persistentClass;
	}

	protected String getPersistentClassName(){
		return this.getPersistentClass() != null ? this.getPersistentClass().getCanonicalName() : "";
	}

	/**
	 * @return the codeClass
	 */
	protected Class<Code> getCodeClass() {
		return codeClass;
	}

	/**
	 * @param persistentClass the persistentClass to set
	 */
	protected void setPersistentClass(Class<T> persistentClass) {
		this.persistentClass = persistentClass;
	}

	/**
	 * @param codeClass the codeClass to set
	 */
	protected void setCodeClass(Class<Code> codeClass) {
		this.codeClass = codeClass;
	}

	/**
	 * @return the transaction
	 */
	public TransactionStrategy getTransaction() {
		return transaction;
	}

	/**
	 * @param transaction the transaction to set
	 */
	public void setTransaction(TransactionStrategy transaction) {
		this.transaction = transaction;
	}


	public final void delete(T entity){
		this.delete(entity, Boolean.TRUE.booleanValue());
	}


	public final void deleteWithoutCommit(T entity){
		this.delete(entity, Boolean.FALSE.booleanValue());
	}

	/** Persist an entity at the repository.
	 *
	 */

	public final T persist(T entity){
		return this.persist(entity, Boolean.TRUE.booleanValue());
	}


	public void commitCurrentTransaction(){
		this.transaction.commit();
	}


	public void beginTransaction(){
		this.transaction.beginTransaction();
	}


	public T findUniqueByCode(Code code) {
		return this.findUniqueByCode(code, false);
	}


	public void deleteBy(Code code){
		T entity = this.findUniqueByCode(code);
		if(entity != null){
			this.delete(entity);
		} else {
			throw new NotFoundUniqueObjectException("Object not found [Code: "+ code + " - PersistenceClass: "+ this.getPersistentClass() +"]to delete.");
		}
	}

	/** Remove all entities from list.
	 *
	 * @param entities to delete.
	 */

	public final void deleteAll(Collection<T> entities){
		for(T t: entities){
			this.delete(t);
		}
	}


	public final void deleteAll(){
		this.deleteAll(this.findAll());
	}


	public void doForAll(Closure<T> closure) {
		Collection<T> entities = this.findAll();
		this.doForAll(entities, closure);
	}


	public void doForAll(Collection<T> entities, Closure<T> closure) {
		if(org.apache.commons.collections.CollectionUtils.isNotEmpty(entities)){
			Collection<T> filteredEntities = this.prepareResultCollection(entities);
			CollectionUtils.forAllDo(filteredEntities, closure);
		}
	}


	public Collection<T> filterBy(Predicate<T> predicate) {
		Collection<T> entities = this.findAll();
		return this.filterBy(entities, predicate);
	}


	public Collection<T> filterBy(Collection<T> entities, Predicate<T> predicate) {
		if(org.apache.commons.collections.CollectionUtils.isNotEmpty(entities)){
			Collection<T> filteredEntities = this.prepareResultCollection(entities);
			CollectionUtils.filter(filteredEntities, predicate);
		}
		return entities;
	}

	/** Get a collection size.
	 *
	 * @param entities collection to get size.
	 * @return collection size.
	 */
	protected Long getCollectionSize(Collection<T> entities){
		return ( entities != null ? Long.valueOf(String.valueOf(entities.size())): 0 );
	}


	public Long count(T exampleInstance) {
		Collection<T> entities = this.findAll(exampleInstance);
		return this.getCollectionSize(entities);
	}


	public Collection<? extends Object> collect(Transformer<T, ? extends Object> transformer) {
		Collection<T> entities = this.findAll();
		return this.collect(entities, transformer);
	}


	public Collection<?> collect(Collection<T> entities, Transformer<T, ? extends Object> transformer) {
		if(org.apache.commons.collections.CollectionUtils.isNotEmpty(entities)){
			Collection<T> filteredEntities = this.prepareResultCollection(entities);
			return CollectionUtils.collect(filteredEntities, transformer);
		}
		return entities;
	}


	public Collection<T> persistAll(Collection<T> entities) {
		List<T> entitiesPersisted = new ArrayList<T>();
		for(T entity: entities){
			T entityPersisted = this.persist(entity);
			entitiesPersisted.add(entityPersisted);
		}
		return entitiesPersisted;
	}


	public T findUnique(String property, Object value) {
		Map<String, Object> propertiesValues = new HashMap<String, Object>();
		propertiesValues.put(property, value);
		return this.findUnique(propertiesValues);
	}


	public T findUniqueOrNull(String property, Object value) {
		Map<String, Object> propertiesValues = new HashMap<String, Object>();
		propertiesValues.put(property, value);
		return this.findUniqueOrNull(propertiesValues);
	}


	public T findUniqueOrNull(Map<String, Object> propertiesValues) {
		Collection<T> entities = this.findAll(propertiesValues);
		if(entities != null && entities.size() > 1){
			throw new UniqueExpectedResultException("A unique result expected for query. It found as result [" + entities.size() +"] entities.");
		}
		return (entities != null && entities.size() == 1 ? entities.iterator().next(): null);
	}


	public T persistWithoutCommit(T entity) {
		return this.persist(entity, Boolean.FALSE.booleanValue());
	}


	public final boolean isForPersistentClass(Class<? extends Object> persistentClass){
		return this.getPersistentClass() != null && this.getPersistentClass().equals(persistentClass);
	}


	public final boolean isForCodeClass(Class<? extends Serializable> codeClass){
		return this.getCodeClass() != null && this.getCodeClass().equals(codeClass);
	}

	public Collection<T> findAll(String property, Object value){
		Map<String, Object> propertiesValues = new HashMap<String, Object>();
		propertiesValues.put(property, value);
		return this.findAll(propertiesValues);
	}

	protected Collection<T> prepareResultCollection(Collection<T> entities){
		Collection<T> resultCollection = this.entitiesFilterManager.appliesFilters(entities);
		resultCollection = this.entitiesActionManager.appliesActions(resultCollection);
		return resultCollection;
	}

	protected Collection<T> prepareResultUnique(T entity){
		return null;
	}


	public void addInstacesAction(FindAllInstancesAction<T> action){
		this.entitiesActionManager.addInstancesAction(action);
	}


	public void addInstacesFilter(FindAllInstancesFilter<T> filter){
		this.entitiesFilterManager.addInstancesFilter(filter);
	}


	public void clearFilters(){
		this.entitiesFilterManager.clearFilters();
	}


	public void clearActions(){
		this.entitiesActionManager.clearActions();
	}

	public void mergeAll(Collection<T> entities) {
		for(T entity: entities){
			this.merge(entity);
		}
	}

	public void updateAll(Collection<T> entities) {
		for(T entity: entities){
			this.update(entity);
		}
	}
}
